package client;

import java.util.*;
import java.util.concurrent.*;
import common.*;

// This is the client gamestate object,
// it is used as a mediator between the GUI and the network thread
// and stores any changes in the gamestate. 
// It is independent of the GUI and can only be updated by the
// network thread, thus making the gamestate server-dependent.

//The gamestate uses a copy-on-read pattern 
//to ensure correctness of synchronization with the EDT.
//This is avoided whenever possible by use of volatile
//fields and a copy-on-receive pattern for the attributes of certain
//objects (such as the player and current round)

public class ClientGameState extends Observable implements Observer {
	//Some internal error codes.
	private final static int GENERIC_ERROR = 1;
	private final static int AUCTION_ERROR = 1<<1;
	private final static int REGISTRATION_ERROR = 1<<2;
	
	private static final int SLEEP_TIME = 50;
	//This queue holds sendables pasesd in by the network thread.
	private final Queue<Sendable> Q = new ConcurrentLinkedQueue<Sendable>();
	
	//The player or user of this client.
	private volatile Player player;
	//The list of all rounds.
	private volatile RoundList RL;
	//The current round.
	private Round current;
	//The chat buffer.
	private ChatBuffer cb;
	//Auction related information storage.
	private EnumMap<Commodity,AuctionList> auctions;
	private EnumMap<Commodity,AuctionList> myAuctions;
	private EnumMap<Commodity,AuctionMatches> trades;
	private volatile int errorState;
	
	//Auction IDs to ignore
	private HashSet<Integer> ignoreID;
	
	public ClientGameState(){
		this(20);
	}
	public ClientGameState(int tw) {
		cb = new ChatBuffer(tw);
		errorState = 0;
	}
	
	//Listen for changes in the gamestate.
	public void listen() {
		cb.add("MOTD : Welcome to the game server.");
		try {
			//While the thread is not interrupted.
			while (!Thread.interrupted()) {
				//If there is something to do do it.
				if (!Q.isEmpty()) stateUpdate(Q.remove());
				//Otherwise Sleep.
				while (Q.isEmpty() && !Thread.interrupted()) Thread.sleep(SLEEP_TIME);
			}
		} catch (InterruptedException e) {
			//Rethrow the interrupt exception.
			Thread.currentThread().interrupt();
		}
	}
	
	//Receive updates from the network client thread
	//and update the state of the game accordingly.
	private void stateUpdate(Sendable s) {
		//This violates OCP, but at least its efficient.
		//Below the protocol used is described briefly.
		
		if (s instanceof RoundList) {
			//If a round list is recieved, it is stored.
			RL = (RoundList)s;
		} else if (s instanceof RegisterResponse) {
			//If a register response is received 
			//then the player object is extracted and set
			//to the current player (unless there was an error)
			RegisterResponse r = (RegisterResponse)s;
			if (r.hasError()) this.addError(REGISTRATION_ERROR);
			else player = r.getPlayer(); 
			
		} else if (s instanceof Player) {
			//If a player is received it is taken to mean an update
			//to a player object in one of the current rounds.
			Player p = (Player)s;
			if (player != null)
				if (player.equals(p)) 
					player.update(p);
					
			if (RL != null)
				for (Round r2 : RL)
					for (Player p2 : r2.getPlayers())
						if (p2.equals(p)) p2.update(p);
				
		} else if (s instanceof Round) {
			//If a round is received that round info is updated.
			//If the current player is in this round
			//then 'current' which holds the current round is set to the
			//received round.
			Round r = (Round)s;
			if (player != null) {
				for (Player p : r.getPlayers()) 
					if (p.equals(player)) {
						if (current == null || !current.equals(r)) initAuctions();
						current = r;
					}
			}
			if (RL != null) {
				boolean found = false;
				for (Round r2 : RL) 
					if (r2.equals(r)) { 
						r2.update(r);
						found = true;
					}
				synchronized(this) {
					if (!found) RL.add(r);
				}
			}
		} else if (s instanceof ChatPacket) {
			//With a chat packet, all we do is add it to the chatbuffer.
			cb.add((ChatPacket)s);
		} else if (s instanceof Auction) {
			//If we get an auction we add the auction to our state.
			Auction a = (Auction)s;
			//Ignores Auctions which may have expired before they arrived.
			if (ignoreID.contains(a.hashCode())) {
				ignoreID.remove(a.hashCode());
				return;
			}
			this.addAuction(a , this.findOwner(a));
			
		} else if (s instanceof AuctionMatch) {
			//If we get an auction match, we do several things :
			//0 : Find the owners of both auctions in the match.
			//1 : Update the scores of both players.
			//2 : If this was our auction remove it from our auction list.
			//3 : Remove the auctions from the player objects.
			Auction a = ((AuctionMatch)s).original;
			Auction b = ((AuctionMatch)s).matchedWith;
			Player player1 = findOwner(a);
			Player player2 = findOwner(b);
			Commodity c = a.commodity;
			synchronized(this) {
				AuctionList L = auctions.get(c);
				AuctionList L2 = myAuctions.get(c);
				AuctionMatches L3 = trades.get(c);
				//If remove failed that means the auction doesnt exist yet?
				if (! L.remove(a) && ignoreID != null) ignoreID.add(a.hashCode());
				if (! L.remove(b) && ignoreID != null) ignoreID.add(b.hashCode());
				
				L3.add((AuctionMatch)s);
				if (player2 != null && player != null && player.equals(player2)) {
					player.buyOrSell(b,a);
					player.removeAuction(b);
					L2.remove(b);
				}
				if (player1 != null && player != null && player.equals(player1)) {
					player.buyOrSell(a,b);
					player.removeAuction(a);
					L2.remove(a);
				}
				if (player1 != null) {
					player1.buyOrSell(a, b);
					player1.removeAuction(a);
				}
				if (player2 != null) {
					player2.buyOrSell(b,a);
					player2.removeAuction(b);
				}
			}
		} else if (s instanceof AuctionResponse) {
			//An auction response is used to determine
			//whether there was an error in processing 
			//an Ask or Bid (such as server-side limit checking).
			
			//If no errors exist the auction in this response
			//is interpreted to be an auction belonging to the
			//current player.
			AuctionResponse a = (AuctionResponse)s;
			if (a.hasError()) { 
				this.addError(AUCTION_ERROR);
			} else {
				addMyAuction(a.responseTo);
			}
		} else if (s instanceof AuctionExpiration) {
			//If an auction expiration occured we do a similar
			//check as done in AuctionMatch except for only 1 player.
			AuctionExpiration e = (AuctionExpiration)s;
			Player p = findOwner(e.expired);
			if (p != null ) p.removeAuction(e.expired);
			synchronized(this) {
				if (p!= null && player != null && p.equals(player)) {
					player.removeAuction(e.expired);
					AuctionList L = myAuctions.get(e.expired.commodity);
					L.remove(e.expired);
				}
				AuctionList L = auctions.get(e.expired.commodity);
				if (!L.remove(e.expired) && ignoreID != null)
					ignoreID.add(e.expired.hashCode());
					
			}
		} else if (s instanceof PlayerLimits) {
			//A playerlimits object when recieved is set on the current
			//player object, so that score can be computed. 
			if (player != null)
				player.setLimits((PlayerLimits)s);
		} else if (s instanceof ReadyPacket) {
			//This packet is sent by the server
			//when a player's ready status has changed thus,
			//we update the player's ready status.
			ReadyPacket s2 = (ReadyPacket)s;
			if (player != null && s2.hasOwner(player)) player.setIsReady(s2.ready);
			if (current != null) {
				for (Player p : current.getPlayers()) {
					if (s2.hasOwner(p))
						p.setIsReady(s2.ready);
				}
			}
		}
		//System.err.println("ClientGameState Changed : " + s);
		this.setChanged();
		this.notifyObservers();
	}
	//This method adds an auction to the global state list of
	//auctions. Synchronization is with 
	private synchronized void addAuction (Auction a, Player p) {
		AuctionList L = auctions.get(a.commodity);
		L.add(a);
		p.addAuction(a);
	}
	//This method adds an auction to the list of the player's auctions.
	private synchronized void addMyAuction(Auction a) {
		AuctionList L = myAuctions.get(a.commodity);
		L.add(a);
		player.addAuction(a);
	}
	//This methods finds the owner of an auction
	//or null if no owner can be found.
	private Player findOwner(Auction a) {
		if (current != null) {
			List<Player> LP = current.getPlayers();
			for (Player p : LP) 
				if (p.ownsAuction(a))
					return p;
		}
		return null;
	}
	//This method initializes the auctions,
	//which is done when the player registers for a new round.
	private void initAuctions() {
		auctions = new EnumMap<Commodity,AuctionList>(Commodity.class);
		myAuctions = new EnumMap<Commodity,AuctionList>(Commodity.class);
		trades = new EnumMap<Commodity,AuctionMatches>(Commodity.class);
		for (Commodity c : Commodity.values()) auctions.put(c, new AuctionList());
		for (Commodity c : Commodity.values()) myAuctions.put(c, new AuctionList());
		for (Commodity c : Commodity.values()) trades.put(c, new AuctionMatches());
		ignoreID = new HashSet<Integer>();
	}
	
	//Public (self explanatory) methods called by any interested party.
	public synchronized Round findRound (int id) {
		if (RL == null) return null;
		if (current != null && current.getID() == id) return current;
		for (Round r : RL) 
			if (r.getID() == id) return r;
		return null;
	}
	public synchronized Round getRound (int index) {
		return (RL == null) ? null : RL.get(index);
	}
	public synchronized Round[] getRounds() {
		Round [] r = new Round[numberOfRounds()];
		for (int i = 0; i<r.length; ++i) r[i] = getRound(i);
		return r;
	}
	
	public synchronized Auction[] getMyAuctions(Commodity c) {
		Auction[] a = new Auction[numberOfMyAuctions(c)];
		for (int i = 0; i<a.length; ++i) a[i] =  getMyAuction(c,i);
		return a;
	}
	public synchronized Auction[] getAllAuctions(Commodity c) {
		Auction[] a = new Auction[numberOfAuctions(c)];
		for (int i = 0; i<a.length; ++i) a[i] =  getAuction(c,i);
		return a;
	}
	//Gets all auctions of a certain type.
	public synchronized Auction[] getAllBids(Commodity c) {
		return getAllX(c,"Bid");
	}
	public synchronized Auction[] getAllAsks(Commodity c) {
		return getAllX(c,"Ask");
	}
	public synchronized Auction[] getAllX(Commodity c, String X) {
		Auction [] a = getAllAuctions(c);
		Auction [] b = new Auction[numberOfAuctions(c)];
		int bptr = 0;
		for (int i =0; i<a.length; ++i) 
			if (a[i].getType().contains(X)) b[bptr++] = a[i];
		Auction [] cc = new Auction[bptr];
		System.arraycopy(b,0,cc,0,cc.length);
		return cc;
	}
	
	public synchronized AuctionMatch[] getTrades(Commodity c) {
		AuctionMatch[] a = new AuctionMatch[numberOfTrades(c)];
		for (int i = 0; i<a.length; ++i) a[a.length-1-i] = trades.get(c).get(i);
		return a;
	}
	
	private synchronized Auction getMyAuction(Commodity c, int index) {
		return myAuctions.get(c).get(index);
	}
	public synchronized int numberOfMyAuctions (Commodity c) {
		return (myAuctions == null) ? 0 : myAuctions.get(c).size();
	}
	private synchronized Auction getAuction (Commodity c, int index) {
		return auctions.get(c).get(index);
	}
	public synchronized int numberOfAuctions (Commodity c) {
		return (auctions == null) ? 0 : auctions.get(c).size();
	}
	public synchronized int numberOfTrades (Commodity c) {
		return (trades == null) ? 0 : trades.get(c).size();
	}
	
	public Player getPlayer() { return player;}
	public Round getCurrentRound() { return current;}
	//This should be private to avoid concurrency problems.
	private RoundList getRoundList() { return RL;}
	public String getChat() { return cb.toString();}
	public int numberOfRounds() { return (RL == null) ? 0 : RL.size();}
	public synchronized List<Player> getCurrentPlayers() {
		return (current == null) ? null : current.getPlayers();
	}
	
	//Some methods for any clients of this object to use
	//to check for errors in the gamestate.
	private void addError(int e) {
		errorState = errorState | e; 
	}
	public boolean hasError() {
		return (errorState != 0);
	}
	public boolean hasGenericError() {
		return hasError(GENERIC_ERROR);
	}
	public boolean hasAuctionError() {
		return hasError(AUCTION_ERROR);
	}
	public boolean hasRegistrationError() {
		return hasError(REGISTRATION_ERROR);
	}
	private boolean hasError (int ERROR_TYPE) {
		if ((errorState & ERROR_TYPE) > 0) {
			errorState = (errorState ^ ERROR_TYPE);
			return true;
		}
		return false;
	}
	
	// Called by the network thread when a new Sendable
	// is recieved from the server.
	public void update(Observable arg0, Object arg1) {
		if (arg1 instanceof Sendable) Q.add((Sendable)arg1);
	}
	//Clears the game state
	//This happens after a server disconnect.
	public void clear() {
		RL = null;
		current = null;
		player = null;
		errorState = 0;
		cb.clear();
	}
	
	public String toString() {
		String s =" Player = " + getPlayer() + "\n";
		s = s + "CurrentRound = " + getCurrentRound() + "\n";
		s = s + "RoundList = " + getRoundList() + "\n";
		if( getPlayer() != null && getPlayer().getLimits() != null ) {
			s = s + "Player Limits:\n";
			for( Commodity c : Commodity.values() ) {
				CommodityLimits limits = getPlayer().getLimits().get( c );
				s = s + c.name() + " : " + limits.bound + " " + limits.quota + " " + limits.isBuyer + "\n"; 
			}
		}
		return s;
	}
	//Private data structures to hold auctions.
	private class AuctionList extends ArrayList<Auction>{
		private static final long serialVersionUID = 2190804150198171843L;
	}
	private class AuctionMatches extends ArrayList<AuctionMatch>{
		private static final long serialVersionUID = 1601740521586530057L;
	}
}
